package com.gitee.wsl.coroutines.strategy.retry
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
 * Runs the [block] of code, following [RetryInstruction]s returned when
 * evaluating the [policy].
 */
@OptIn(ExperimentalContracts::class)
suspend inline fun <T> retry(
    noinline policy: RetryPolicy<Throwable> = constantDelay(50) + limitAttempts(5),
    block: RetryScope.() -> T
): T {
    contract {
        callsInPlace(policy, InvocationKind.UNKNOWN)
        callsInPlace(block, InvocationKind.AT_LEAST_ONCE)
    }

    val status = RetryStatus()
    val scope = CoroutineRetryScope(status, currentCoroutineContext().retryRandom)

    lateinit var mostRecentFailure: Throwable

    while (true) {
        try {
            return block(scope)
        } catch (t: Throwable) {
            /* avoid swallowing CancellationExceptions */
            if (t is CancellationException) {
                throw t
            }

            val failure = DelegatedRetryFailure(t, scope)
            val instruction = failure.policy()

            status.incrementAttempts()

            if (instruction == StopRetrying) {
                mostRecentFailure = t
                break
            } else if (instruction == ContinueRetrying) {
                status.previousDelay = 0
                continue
            } else {
                val delay = instruction.delayMillis
                delay(delay)
                status.previousDelay = delay
                status.incrementCumulativeDelay(delay)
            }
        }
    }

    throw mostRecentFailure
}

suspend inline fun <T> retry(
    maxRetries:Int ,
    delayMillis:Long = 50,
    block: RetryScope.() -> T
): T = retry(policy = constantDelay(delayMillis) + limitAttempts(maxRetries), block )


suspend inline fun <T> retry(
    crossinline predicate: RetryFailure<Throwable>.() -> Boolean,
    block: RetryScope.() -> T
): T = retry(policy = retryIf(predicate),block)


suspend inline fun <T,reified E:Throwable> retry(
    block: RetryScope.() -> T
) = retry(predicate = { this.reason is E },block)


suspend inline fun <T,reified E:Throwable> retry(
    maxRetries:Int,
    block: RetryScope.() -> T
) = retry(policy = limitAttempts(maxRetries) + retryIf{ this.reason is E } ,block)


suspend inline fun <T> retry(
    maxDelayMillis:Long,
    delayMillis:Long = 50,
    block: RetryScope.() -> T
): T = retry(policy = constantDelay(delayMillis).maxDelay(maxDelayMillis), block )

suspend inline fun <T> retry(
    maxDelayMillis:Long,
    maxRetries:Int,
    delayMillis:Long = 50,
    block: RetryScope.() -> T
): T = retry(policy = limitAttempts(maxRetries) + constantDelay(delayMillis).maxDelay(maxDelayMillis), block )





